feat(nodes): add tool_gohighlevel CRM tool node - #1695
Open
charliegillet wants to merge 3 commits into
Open
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Exposes the GoHighLevel (LeadConnector) v2 REST API to agents: 101 tools across 17 resource groups (contacts, notes, tasks, opportunities, pipelines, conversations, messages, calendars, calendar groups, appointments, appointment notes, custom fields, custom values, location tags, locations, businesses, users), plus a generic
requesttool that reaches any remaining v2 endpoint through the same auth, rate-limit and read-only layer.Full coverage is more tools than an LLM chooses between reliably, so
gohighlevel.toolGroupscontrols which groups publish. The default ten groups publish 74 tools;["all"]publishes everything. Filtering happens inIInstance._collect_tool_methods(), so a gated tool is invisible totool.queryand refused bytool.invokealike.Read-only mode hides write tools rather than only refusing them. This differs from
tool_pipedrive, which refuses at invoke time. A tool the agent can only fail on is wasted context, soreadOnly: truepublishes 45 tools instead of 102 and leaks none of the 57 write-stamped tools.Authentication: Private Integration Token only
No OAuth, deliberately. GoHighLevel refresh tokens are single-use and rotating, verbatim from their OAuth docs: "Once you use a Refresh Token to obtain a new Access Token, the original Refresh Token becomes invalid." Access tokens live 24 hours and a node has nowhere durable to persist the replacement. An OAuth credential would therefore work for ~24h, refresh, discard the new token it cannot store, run another ~24h, then present the invalidated original and die, unattended, with no config-only recovery: re-pasting the burned token does nothing. Adding OAuth needs a durable secret store the node can write on every refresh plus a lock against concurrent runs, which is what HighLevel's own SDK ships MongoDB/Redis session storage for.
Consequence worth naming for whoever supports this: one credential reaches exactly one sub-account. Agency fan-out is refused at the API (
/oauth/installedLocationsreturns 401,/locations/searchreturns 403, both confirmed live), so N sub-accounts means N node instances.locationIdis a required config field because the PIT is opaque and carries no location.Client details specific to GoHighLevel
Each of these was verified against a live sub-account, not taken from the docs:
Versionis per operation, never global:2021-04-15for calendars and conversations,2021-07-28elsewhere. Sending one value globally breaks that whole family with a 4xx that reads like an auth failure. (n8n's connector hardcodes one value and is wrong here.)/contacts/{id}/appointmentsreturnsevents), socall()does not unwrap and each tool names its key.GET /opportunities/searchrequireslocation_idin snake case and rejects the camelCase form with a 422. Every other endpoint is camelCase.locationIdor agency-scope refusal, so retrying it burns budget and hides the real error.x-ratelimit-daily-resetis a duration in milliseconds, not an epoch; sleeping on it as a timestamp would produce a ~55-year wait.Retry-After(triggered deliberately and captured; it arrives on request 26 of a 25-request window), so backoff comes fromx-ratelimit-interval-milliseconds.messagesometimes a string and sometimes a list, and a bodystatusCodethat can disagree with the HTTP status (POST /contacts/answers HTTP 400 carryingstatusCode: 422), so status always comes from the response.Testing
203 unit tests, no credentials and no network. The largest class is
TestVersionHeader(86 tests), because a wrongVersionis the most expensive silent mistake available on this API._build.yamlhas no pytest step, itsTeststep runs./builder test(which does not include node pytest), andos-matrix.yml(the workflow that would) isworkflow_dispatchplus a monthly cron rather than a per-PR check. Node pytest lives behindbuilder nodes:test, which no workflow invokes. So the green checks on this PR mean lint, secrets, links, dependency constraints, the shell contract, and a three-OS engine build, not this node's tests. To run them:That is a pre-existing repo gap rather than anything this PR introduces, but it is worth a reviewer knowing, and it means the 203 passing tests rest on my local run rather than on CI.
Live suite (
test_tools.py, 87 tests) behindGHL_PIT/GHL_LOCATION_ID, with record-creating tests additionally behindGHL_ALLOW_WRITES. Run against a real trial sub-account before this PR went up.Exercised live, full create/read/update/delete lifecycles: contacts (incl. tags, followers, upsert, duplicate check), contact notes, contact tasks, opportunities (incl. a status transition confirmed by re-read), pipelines, conversations, calendars, appointments, custom fields, custom values, location tags, businesses, users, locations.
The live suite found a real bug that no stubbed test could:
appointment_getreturned{}for every appointment.GET /calendars/events/appointments/{eventId}answers underappointment, while both published specs declareevent. The unwrapper only strips a key the payload carries, so the whole envelope reached the cleaner, which matched none of its read keys and emitted nothing. Fixed, and the fix was then verified end to end against the live API (create calendar, create appointment, read back a populated record, delete both). Every other envelope key in all 101 tools was subsequently audited against the OpenAPI specs.Not proven, stated plainly
message_send, both schedule-cancels,message_transcription_getandmessage_email_getneed a phone or email provider provisioned on the sub-account; a trial has neither.contact_workflow_add/contact_workflow_removecan never be proven from a sub-account, because GoHighLevel publishes no endpoint that lists workflows, so a validworkflowIdcannot be obtained.contact_duplicate_check's 200 body is undocumented in both specs and unattested anywhere; the code reads it tolerantly but the shape is a guess.rocketlibandai.common. The three-OS build passes with the node present, but that builds the engine rather than importing this node or registering its tools. The contract suite and the IDE canvas have not been run. This is the main reason it is a draft.appointment_getbug shows the specs can be wrong.Scope, and the Phase 1 framing in the issue
The issue frames Phase 1 as contacts, opportunities, conversations/messages, calendars/appointments. That is 76 of these 101 tools. The other 25 are the id-resolution ring Phase 1 does not work without:
contact_updatecannot set a custom field withoutcustom_field_listto resolve the field id, andassignedToneeds a user lookup. Breadth is implemented, publication is gated, and the default group set maps onto the issue's Phase 1 list.186 operations were inventoried; 101 are here. 74 are deferred and 11 are out of scope (9 agency-only with the refusal confirmed live, 2 needing a conversation-provider marketplace app). Suggest tracking the deferred 74 as a checklist on a follow-up issue rather than losing them when this closes #1676.
Also considered: bridging their MCP server
GoHighLevel ships an official MCP server, so it was measured rather than dismissed: 36 tools against 576 raw v2 operations (6.3%), only 19 of which touch the 186 operations above (10.2%). It is sub-account only and gates irreversible writes behind an interactive safety confirmation an unattended pipeline node cannot answer.
tool_mcp_clientcan still be pointed athttps://services.leadconnectorhq.com/mcp/with zero code as a complementary exploratory path, but it does not meet the "no functional limitations" bar in the issue.Notes for review
requestescape hatch has nooutput_schema, deliberately: it returns whatever the endpoint returns. Flagging in case a permissive schema is wanted anyway.docs/README-nodes.md: the row is appended at the end of the Agent Tools table and the node-count line on line 14 is untouched. It is already wrong ondevelopand feat(nodes): add tool_pipedrive CRM tool node #1687 also edits it, so touching it buys a conflict for zero accuracy.tool_pipedrive) is oneCHANGELOG.mdbullet. Whichever merges second rebases.project()helper that raises when a non-empty record projects to nothing. That is the failure mode behind theappointment_getbug, and it is currently indistinguishable from "not found" for about ten read tools. It changes failure semantics across every read tool, so it did not belong in this PR.Fixes #1676
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation